﻿// Jinx AJAX Framework
// Copyright (C) 2008 Blake Hegerle
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Reflection;

namespace Core.Json
{
    using ObjectFactory = Func<Type, object>;
    using System.Collections;

    public class Member : Tuple<string, object>
    {
        public Member(string Name, object Value)
            : base(Name, Value)
        {
        }

        public string Name
        {
            get { return Item1; }
        }

        public object Value
        {
            get { return Item2; }
        }
    }

    public class TypeCannotBeConstructed : Exception
    {
        internal TypeCannotBeConstructed(Type T)
            : base(
                string.Format(
                    "Could not consruct an object of type {0} because it does not have a public, parameterless constructor",
                    T.Name))
        {
        }
    }

    public static class ObjectReflection
    {
        public static IEnumerable<Member> Members(this object Obj)
        {
            var Members =
                Obj.GetType().GetMembers(BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Instance |
                                         BindingFlags.Public);

            foreach (var Member in Members)
            {
                object Value;

                if (Get(Obj, Member, out Value))
                    yield return new Member(Member.Name, Value);
            }
        }

        public static object Get(this object Obj, string MemberName)
        {
            if (Obj == null)
                throw new ArgumentNullException("Obj");

            var Member = GetMember(Obj, MemberName,
                                   BindingFlags.Public |
                                   BindingFlags.Instance |
                                   BindingFlags.GetField |
                                   BindingFlags.GetProperty);

            object Value;
            return Get(Obj, Member, out Value) ? Value : null;
        }

        private static bool Get(object Obj, MemberInfo Member, out object Value)
        {
            Value = null;

            if (Member is PropertyInfo)
            {
                var Prop = Member as PropertyInfo;

                if (Prop.GetIndexParameters().Length != 0)
                    return false;

                Value = Prop.GetValue(Obj, null);
            }
            else if (Member is FieldInfo)
            {
                var Field = Member as FieldInfo;

                Value = Field.GetValue(Obj);
            }
            else
            {
                return false;
            }

            return true;
        }

        public static void CoerciveSet(this object Obj, string Name, object Value)
        {
            Obj.CoerciveSet(Name, Value, T =>
                                             {
                                                 var Constructor = T.GetConstructor(new Type[0]);

                                                 if (Constructor == null)
                                                     throw new TypeCannotBeConstructed(T);

                                                 return Constructor.Invoke(null);
                                             });
        }

        public static void CoerciveSet(this object Obj, string Name, object Value, ObjectFactory ObjectFactory)
        {
            var Member = GetMember(Obj, Name,
                                   BindingFlags.Public |
                                   BindingFlags.Instance |
                                   BindingFlags.SetField |
                                   BindingFlags.SetProperty);

            if (Member is PropertyInfo)
            {
                var Prop = Member as PropertyInfo;

                Value = Coerce(Value, Prop.PropertyType, ObjectFactory);

                Prop.SetValue(Obj, Value, null);
            }
            else if (Member is FieldInfo)
            {
                var Field = Member as FieldInfo;

                Value = Coerce(Value, Field.FieldType, ObjectFactory);

                Field.SetValue(Obj, Value);
            }
        }

        private static MemberInfo GetMember(object Obj, string Name, BindingFlags Flags)
        {
            if (Obj == null)
                throw new ArgumentNullException("Obj");

            var Candidates = Obj.GetType().GetMember(Name, MemberTypes.Field | MemberTypes.Property, Flags);

            if (Candidates.Length != 1)
                throw new MissingMemberException("Could not set " + Name +
                                                 " because there is no field or property by that name");

            return Candidates[0];
        }

        public static object Coerce(object Value, Type Type, ObjectFactory ObjectFactory)
        {
            if (Value != null)
            {
                if (Value is IConvertible)
                {
                    var iConv = Value as IConvertible;

                    if (Type == typeof(float) || Type == typeof(float?))
                        Value = iConv.ToSingle(null);

                    if (Type == typeof(decimal) || Type == typeof(decimal?))
                        Value = iConv.ToDecimal(null);

                    if (Type == typeof(byte))
                        Value = iConv.ToByte(null);

                    if (Type == typeof(sbyte))
                        Value = iConv.ToSByte(null);

                    if (Type == typeof(ushort))
                        Value = iConv.ToUInt16(null);

                    if (Type == typeof(short))
                        Value = iConv.ToInt16(null);

                    if (Type == typeof(uint))
                        Value = iConv.ToUInt32(null);

                    if (Type == typeof(int))
                        Value = iConv.ToInt32(null);

                    if (Type == typeof(ulong))
                        Value = iConv.ToUInt64(null);
                }

                var Dict = Value as IDictionary<string, object>;
                if (Dict != null)
                    Value = Coerce(Dict, Type, ObjectFactory);

                if (Type.IsArray && Value is IEnumerable)
                {
                    var Method = typeof(ObjectReflection).GetMethod("CoerceArray", BindingFlags.Static | BindingFlags.NonPublic);

                    var BoundMethod = Method.MakeGenericMethod(Type.GetElementType());

                    Value = BoundMethod.Invoke(null, new object[] { Value, ObjectFactory });
                }
            }

            return Value;
        }

        private static object Coerce(IDictionary<string, object> MemberMap, Type Type, ObjectFactory ObjectFactory)
        {
            if (Type == typeof(IDictionary<string, object>))
                return MemberMap;

            var Constructor = Type.GetConstructor(new Type[0]);

            if (Constructor == null)
                throw new Exception();

            var Target = Constructor.Invoke(null);

            foreach (var Member in MemberMap)
                Target.CoerciveSet(Member.Key, Member.Value);

            return Target;
        }

        private static T[] CoerceArray<T>(IEnumerable Enum, ObjectFactory ObjectFactory)
        {
            var List = new List<T>();

            foreach (var e in Enum)
                List.Add((T)Coerce(e, typeof(T), ObjectFactory));

            return List.ToArray();
        }
    }
}